Design Music Streaming Service like Spotify

Last Updated: December 19, 2025

Ashish

Ashish Pratap Singh

hard

In this chapter, we will explore the low-level design of a Spotify like service in detail.

Lets start by clarifying the requirements:

1. Clarifying Requirements

Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.

Here is an example of how a conversation between the candidate and the interviewer might unfold:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • Support both free and premium tiers for users
  • Support adding songs, albums, playlists, and artists
  • Allow users to play songs, albums, and playlists uniformly
  • Support playback controls: play, pause, and skip (next track).
  • Support creation and management of playlists (create, delete, add/remove songs)
  • Allow users to search for songs by title or search for artists by name
  • Generate song recommendations using pluggable strategies (e.g., genre-based, randomized).

1.2 Non-Functional Requirements

  • Modularity: The system should be composed of well-defined modules
  • Extensibility: The design should be flexible to allow future additions
  • Maintainability: Code should be modular, easy to test, and cleanly organized

2. Identifying Core Entities

Core entities are the foundational building blocks of our system. We identify them by analyzing key nouns (e.g., song, album, artist, playlist, user, playback history) and actions (e.g., stream, search, add, browse, reorder) from the functional requirements. These typically translate directly into classes, enums, or interfaces in an object-oriented design.

Let’s walk through the requirements and extract the relevant entities:

1. Support playing individual songs, albums, and playlists uniformly.

This points to the core content entities: SongAlbum, and Playlist. The need to treat them "uniformly" is a strong indicator for the Composite design pattern. This leads to a Playable interface that all three content types implement, allowing the Player to handle them interchangeably.

2. Support playback controls (play, pause, next).

This suggests a central Player entity to manage the playback queue and state.

3. Support both free and premium user tiers with different playback experiences.

The system needs a User entity to represent the listener. The difference in playback (e.g., with or without ads) is a behavioral concern, which is handled by the Strategy pattern. This introduces a PlaybackStrategy interface with different implementations (FreePlaybackStrategyPremiumPlaybackStrategy). A SubscriptionTier enum (FREE, PREMIUM) is used to determine which strategy a User gets.

4. Allow users to follow artists.

This introduces the Artist entity.

5. Provide search and recommendation functionalities.

These cross-cutting concerns are best encapsulated in dedicated service classes. A SearchService is needed to handle queries for songs and artists. Similarly, a RecommendationService is responsible for generating song suggestions.

6. Provide a simplified, high-level interface to the system.

To manage the interactions between all these components, an MusicStreamingSystem class acts as a Facade and Singleton. It provides a simple entry point to interact with the entire system.

These core entities define the essential abstractions of a music streaming service like Spotify and will guide the structure of your low-level design and class diagrams.

3. Designing Classes and Relationships

This section breaks down the system's architecture into its fundamental classes, their responsibilities, and the relationships that connect them. We also explore the key design patterns that provide robustness and flexibility to the solution.

3.1 Class Definitions

The system is composed of several types of classes, each with a distinct role.

Enums

Enums
  • SubscriptionTier: Defines the user's subscription level (FREE, PREMIUM), which dictates their playback experience.
  • PlayerStatus: Represents the current state of the music player (PLAYING, PAUSED, STOPPED).

Data Classes

Song

A data class representing an individual music track.

It holds details like title, artist, and duration. It also implements the Playable interface, making it a leaf node in the Composite pattern.

Album

Album

A data class that acts as a collection of Songs. It implements the Playable interface, making it a composite node.

Playlist

A data class representing a user-curated list of Songs. It also implements the Playable interface, acting as another composite node.

Core Classes

Playable (Interface)

The Component role in the Composite pattern. It defines a common interface (getTracks) for both individual songs and collections (albums, playlists), allowing them to be treated uniformly.

Artist

Represents a music artist or band.

Artist

It acts as a concrete Subject, maintaining a list of followers (observers) and notifying them when a new Album is released.

User

Represents a listener on the platform.

It acts as a concrete Observer by implementing ArtistObserver, allowing it to "follow" artists. It is configured with a PlaybackStrategy based on its subscription tier. Its construction is handled by a nested Builder.

Player

The core music player.

Player

It acts as the Context for the State pattern, delegating actions like play and pause to its current PlayerState object. It manages the playback queue.

SearchService & RecommendationService

Service-layer classes that encapsulate specific business logic for searching the catalog and generating recommendations.

MusicStreamingSystem (Singleton & Facade)

MusicStreamingSystem

The primary entry point for the application.

It provides a simple, unified API to the client, hiding the complex interactions between the various services, data models, and patterns.

3.2 Class Relationships

The relationships between classes define the system's structure and data flow.

Composition

  • MusicStreamingSystem "has-a" collection of Users, Songs, and Artists, managing their lifecycle.
  • An Album is composed of a list of Songs.
  • A Playlist is composed of a list of Songs.

Association

  • A Player is associated with a single PlayerState at any given time.
  • A User is associated with a PlaybackStrategy.
  • An Artist (Subject) is associated with a list of ArtistObservers (Users).
  • A Song is associated with its Artist.
  • A Command object holds a reference to the Player instance (the receiver) on which it will execute.

Inheritance / Implementation

  • Song, Album, and Playlist all implement the Playable interface.
  • Artist extends the abstract Subject class.
  • User implements the ArtistObserver interface.
  • Concrete state classes (PlayingState, etc.) implement the PlayerState interface.
  • Concrete strategy classes (FreePlaybackStrategy, etc.) implement the PlaybackStrategy interface.
  • Concrete command classes (PlayCommand, etc.) implement the Command interface.

Dependency

  • The client (MusicStreamingDemo) depends on the MusicStreamingSystem facade to interact with the system.
  • The Player depends on a PlaybackStrategy (provided by the User) to play a song.
  • The User.Builder depends on the PlaybackStrategy.getStrategy factory method.

3.3 Key Design Patterns

Strategy Pattern

This pattern is used to make core algorithms interchangeable.

Playback

PlaybackStrategy

The PlaybackStrategy allows the playback behavior (ad-free vs. ad-supported) to be assigned to a User dynamically based on their subscription tier.

Recommendation

RecommendationStrategy

The RecommendationStrategy allows for different recommendation algorithms to be swapped out easily.

State Pattern

PlayerState

The lifecycle of the Player is managed using the State pattern. The Player (Context) delegates its behavior to different PlayerState objects (PlayingState, PausedState, StoppedState). This cleanly separates state-specific logic and makes managing player actions robust.

Observer Pattern

ArtistObserver

This pattern is used for the "follow artist" feature. The Artist (Subject) notifies all subscribed Users (Observers) when a new album is released, decoupling the artist's actions from the user notification system.

Composite Pattern

The Playable interface allows the Player to treat individual Songs (leafs) and collections like Albums or Playlists (composites) uniformly. The player.load() method can accept any Playable object without needing to know its specific type.

Command Pattern

Command

This pattern encapsulates a player action (e.g., "play", "pause") into a standalone object (PlayCommand, PauseCommand). This decouples the client that issues the request (e.g., a UI button) from the Player object that knows how to perform it.

Builder Pattern

The User.Builder provides a fluent, step-by-step API for constructing a User object, especially useful for setting up the correct PlaybackStrategy based on subscription details.

Factory Method (Static Factory)

The PlaybackStrategy.getStrategy() method acts as a simple factory, encapsulating the logic for creating the correct strategy instance based on the user's SubscriptionTier.

Facade Pattern

The MusicStreamingSystem class serves as a facade. It provides a simple, high-level API (registerUser, searchSongsByTitle, getPlayer) that hides the complex internal workflows involving players, states, strategies, and observers.

Singleton Pattern

MusicStreamingSystem is implemented as a singleton to ensure a single, globally accessible point of control for the entire application, managing all users, content, and services.

3.4 Full Class Diagram

Music Streaming Service Class Diagram

4. Implementation

4.1 Enums

1class SubscriptionTier(Enum):
2    FREE = "FREE"
3    PREMIUM = "PREMIUM"
4
5class PlayerStatus(Enum):
6    PLAYING = "PLAYING"
7    PAUSED = "PAUSED"
8    STOPPED = "STOPPED"
  • SubscriptionTier: Determines the user’s playback strategy (ad-supported vs ad-free).
  • PlayerStatus: Reflects the current state of the player.

4.2 Song and Playable (Composite Pattern)

Song implements Playable, enabling uniform treatment alongside Album and Playlist. This demonstrates the Composite pattern for treating individual songs and groups (albums/playlists) interchangeably.

1class Playable(ABC):
2    @abstractmethod
3    def get_tracks(self) -> List['Song']:
4        pass
1class Song(Playable):
2    def __init__(self, song_id: str, title: str, artist: 'Artist', duration_in_seconds: int):
3        self._id = song_id
4        self._title = title
5        self._artist = artist
6        self._duration_in_seconds = duration_in_seconds
7    
8    def get_tracks(self) -> List['Song']:
9        return [self]
10    
11    def __str__(self) -> str:
12        return f"'{self._title}' by {self._artist.get_name()}"
13    
14    @property
15    def id(self) -> str:
16        return self._id
17    
18    @property
19    def title(self) -> str:
20        return self._title
21    
22    @property
23    def artist(self) -> 'Artist':
24        return self._artist

Album

1class Album(Playable):
2    def __init__(self, title: str):
3        self._title = title
4        self._tracks: List[Song] = []
5    
6    def add_track(self, song: Song):
7        self._tracks.append(song)
8    
9    def get_tracks(self) -> List[Song]:
10        return self._tracks.copy()
11    
12    def get_title(self) -> str:
13        return self._title

Playlist

1class Playlist(Playable):
2    def __init__(self, name: str):
3        self._name = name
4        self._tracks: List[Song] = []
5    
6    def add_track(self, song: Song):
7        self._tracks.append(song)
8    
9    def get_tracks(self) -> List[Song]:
10        return self._tracks.copy()

4.3 Artist and Observer Pattern

This pattern allows users to "follow" an artist and receive notifications when that artist releases a new album.

Observer

1class ArtistObserver(ABC):
2    @abstractmethod
3    def update(self, artist: 'Artist', new_album: 'Album'):
4        pass
5
6class Subject:
7    def __init__(self):
8        self._observers: List[ArtistObserver] = []
9    
10    def add_observer(self, observer: ArtistObserver):
11        self._observers.append(observer)
12    
13    def remove_observer(self, observer: ArtistObserver):
14        if observer in self._observers:
15            self._observers.remove(observer)
16    
17    def notify_observers(self, artist: 'Artist', album: 'Album'):
18        for observer in self._observers:
19            observer.update(artist, album)

Artist

1class Artist(Subject):
2    def __init__(self, artist_id: str, name: str):
3        super().__init__()
4        self._id = artist_id
5        self._name = name
6        self._discography: List['Album'] = []
7    
8    def release_album(self, album: 'Album'):
9        self._discography.append(album)
10        print(f"[System] Artist {self._name} has released a new album: {album.get_title()}")
11        self.notify_observers(self, album)
12    
13    @property
14    def id(self) -> str:
15        return self._id
16    
17    def get_name(self) -> str:
18        return self._name

User

1class User(ArtistObserver):
2    def __init__(self, user_id: str, name: str, playback_strategy: PlaybackStrategy):
3        self._id = user_id
4        self._name = name
5        self._playback_strategy = playback_strategy
6        self._followed_artists: Set[Artist] = set()
7    
8    def follow_artist(self, artist: Artist):
9        self._followed_artists.add(artist)
10        artist.add_observer(self)
11    
12    def update(self, artist: Artist, new_album: Album):
13        print(f"[Notification for {self._name}] Your followed artist {artist.get_name()} "
14              f"just released a new album: {new_album.get_title()}!")
15    
16    @property
17    def playback_strategy(self) -> PlaybackStrategy:
18        return self._playback_strategy
19    
20    @property
21    def id(self) -> str:
22        return self._id
23    
24    def get_name(self) -> str:
25        return self._name
26    
27    class Builder:
28        def __init__(self, name: str):
29            self._id = str(uuid.uuid4())
30            self._name = name
31            self._playback_strategy = None
32        
33        def with_subscription(self, tier: SubscriptionTier, songs_played: int) -> 'User.Builder':
34            self._playback_strategy = PlaybackStrategy.get_strategy(tier, songs_played)
35            return self
36        
37        def build(self) -> 'User':
38            return User(self._id, self._name, self._playback_strategy)

The Artist (Subject) doesn't know anything about the User class. It only knows it has a list of ArtistObserver objects to notify. This decouples the content creators from the content consumers.

4.4 Player

The Player class holds the current state and delegates all actions to it.

1class Player:
2    def __init__(self):
3        self._state = StoppedState()
4        self._status = PlayerStatus.STOPPED
5        self._queue: List[Song] = []
6        self._current_index = -1
7        self._current_song: Optional[Song] = None
8        self._current_user: Optional[User] = None
9    
10    def load(self, playable: Playable, user: User):
11        self._current_user = user
12        self._queue = playable.get_tracks()
13        self._current_index = 0
14        print(f"Loaded {len(self._queue)} tracks for user {user.get_name()}.")
15        self._state = StoppedState()
16    
17    def play_current_song_in_queue(self):
18        if 0 <= self._current_index < len(self._queue):
19            song_to_play = self._queue[self._current_index]
20            self._current_user.playback_strategy.play(song_to_play, self)
21    
22    def click_play(self):
23        self._state.play(self)
24    
25    def click_pause(self):
26        self._state.pause(self)
27    
28    def click_next(self):
29        if self._current_index < len(self._queue) - 1:
30            self._current_index += 1
31            self.play_current_song_in_queue()
32        else:
33            print("End of queue.")
34            self._state.stop(self)
35    
36    def change_state(self, state: PlayerState):
37        self._state = state
38    
39    def set_status(self, status: PlayerStatus):
40        self._status = status
41    
42    def set_current_song(self, song: Song):
43        self._current_song = song
44    
45    def has_queue(self) -> bool:
46        return len(self._queue) > 0

4.5 PlayerState

The player's behavior changes drastically based on whether it is Playing, Paused, or Stopped. The State pattern is perfect for managing these transitions cleanly.

1class PlayerState(ABC):
2    @abstractmethod
3    def play(self, player: 'Player'):
4        pass
5    
6    @abstractmethod
7    def pause(self, player: 'Player'):
8        pass
9    
10    @abstractmethod
11    def stop(self, player: 'Player'):
12        pass
13
14class PausedState(PlayerState):
15    def play(self, player: 'Player'):
16        print("Resuming playback.")
17        player.change_state(PlayingState())
18        player.set_status(PlayerStatus.PLAYING)
19    
20    def pause(self, player: 'Player'):
21        print("Already paused.")
22    
23    def stop(self, player: 'Player'):
24        print("Stopping playback from paused state.")
25        player.change_state(StoppedState())
26        player.set_status(PlayerStatus.STOPPED)
27
28class PlayingState(PlayerState):
29    def play(self, player: 'Player'):
30        print("Already playing.")
31    
32    def pause(self, player: 'Player'):
33        print("Pausing playback.")
34        player.change_state(PausedState())
35        player.set_status(PlayerStatus.PAUSED)
36    
37    def stop(self, player: 'Player'):
38        print("Stopping playback.")
39        player.change_state(StoppedState())
40        player.set_status(PlayerStatus.STOPPED)
41
42class StoppedState(PlayerState):
43    def play(self, player: 'Player'):
44        if player.has_queue():
45            print("Starting playback.")
46            player.change_state(PlayingState())
47            player.set_status(PlayerStatus.PLAYING)
48            player.play_current_song_in_queue()
49        else:
50            print("Queue is empty. Load songs to play.")
51    
52    def pause(self, player: 'Player'):
53        print("Cannot pause. Player is stopped.")
54    
55    def stop(self, player: 'Player'):
56        print("Already stopped.")

Each state class encapsulates the logic for that specific state. For example, clickPlay() in the PlayingState does nothing, but in the StoppedState, it starts the playback and transitions the player to the PlayingState. This avoids a large, complex if/else block in the Player class.

4.6 PlaybackStrategy

The playback experience differs significantly for FREE vs. PREMIUM users. The Strategy pattern allows us to define these different behaviors and assign them to users at runtime.

1class PlaybackStrategy(ABC):
2    @abstractmethod
3    def play(self, song: Song, player: 'Player'):
4        pass
5    
6    @staticmethod
7    def get_strategy(tier: SubscriptionTier, songs_played: int) -> 'PlaybackStrategy':
8        if tier == SubscriptionTier.PREMIUM:
9            return PremiumPlaybackStrategy()
10        else:
11            return FreePlaybackStrategy(songs_played)
12
13class FreePlaybackStrategy(PlaybackStrategy):
14    SONGS_BEFORE_AD = 3
15    
16    def __init__(self, initial_songs_played: int):
17        self._songs_played = initial_songs_played
18    
19    def play(self, song: Song, player: 'Player'):
20        if self._songs_played > 0 and self._songs_played % self.SONGS_BEFORE_AD == 0:
21            print("\n>>> Playing Advertisement: 'Buy Spotify Premium for ad-free music!' <<<\n")
22        player.set_current_song(song)
23        print(f"Free User is now playing: {song}")
24        self._songs_played += 1
25
26class PremiumPlaybackStrategy(PlaybackStrategy):
27    def play(self, song: Song, player: 'Player'):
28        player.set_current_song(song)
29        print(f"Premium User is now playing: {song}")

Each strategy class encapsulates a different playback algorithm. FreePlaybackStrategy includes logic for inserting ads, while PremiumPlaybackStrategy does not.

4.7 RecommendationStrategy

Applies the Strategy pattern to generate different types of song recommendations.

1class RecommendationStrategy(ABC):
2    @abstractmethod
3    def recommend(self, all_songs: List[Song]) -> List[Song]:
4        pass
5
6class GenreBasedRecommendationStrategy(RecommendationStrategy):
7    def recommend(self, all_songs: List[Song]) -> List[Song]:
8        print("Generating genre-based recommendations (simulated)...")
9        shuffled = all_songs.copy()
10        random.shuffle(shuffled)
11        return shuffled[:5]

4.8 Command

This pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

1class Command(ABC):
2    @abstractmethod
3    def execute(self):
4        pass
5
6class PlayCommand(Command):
7    def __init__(self, player: Player):
8        self._player = player
9    
10    def execute(self):
11        self._player.click_play()
12
13class PauseCommand(Command):
14    def __init__(self, player: Player):
15        self._player = player
16    
17    def execute(self):
18        self._player.click_pause()
19
20class NextTrackCommand(Command):
21    def __init__(self, player: Player):
22        self._player = player
23    
24    def execute(self):
25        self._player.click_next()

Each Command object encapsulates a single action (e.g., "play"). This is useful for creating UI elements like buttons. A "Play" button can be configured with a PlayCommand object, and its onClick handler would simply call command.execute(). The button doesn't need to know anything about the Player's internal workings.

4.9 RecommendationService

1class RecommendationService:
2    def __init__(self, strategy: RecommendationStrategy):
3        self._strategy = strategy
4    
5    def set_strategy(self, strategy: RecommendationStrategy):
6        self._strategy = strategy
7    
8    def generate_recommendations(self, all_songs: List[Song]) -> List[Song]:
9        return self._strategy.recommend(all_songs)

4.10 Search Service

Encapsulates searching and recommending functionality for the catalog.

1class SearchService:
2    def search_songs_by_title(self, songs: List[Song], query: str) -> List[Song]:
3        return [s for s in songs if query.lower() in s.title.lower()]
4    
5    def search_artists_by_name(self, artists: List[Artist], query: str) -> List[Artist]:
6        return [a for a in artists if query.lower() in a.get_name().lower()]

4.11 MusicStreamingSystem (Facade + Singleton)

1class MusicStreamingSystem:
2    _instance = None
3    _lock = threading.Lock()
4    
5    def __new__(cls):
6        if cls._instance is None:
7            with cls._lock:
8                if cls._instance is None:
9                    cls._instance = super().__new__(cls)
10                    cls._instance._initialized = False
11        return cls._instance
12    
13    def __init__(self):
14        if not self._initialized:
15            self._users: Dict[str, User] = {}
16            self._songs: Dict[str, Song] = {}
17            self._artists: Dict[str, Artist] = {}
18            self._player = Player()
19            self._search_service = SearchService()
20            self._recommendation_service = RecommendationService(GenreBasedRecommendationStrategy())
21            self._initialized = True
22    
23    @classmethod
24    def get_instance(cls):
25        return cls()
26    
27    def register_user(self, user: User):
28        self._users[user.id] = user
29    
30    def add_song(self, song_id: str, title: str, artist_id: str, duration: int) -> Song:
31        song = Song(song_id, title, self._artists[artist_id], duration)
32        self._songs[song.id] = song
33        return song
34    
35    def add_artist(self, artist: Artist):
36        self._artists[artist.id] = artist
37    
38    def search_songs_by_title(self, title: str) -> List[Song]:
39        return self._search_service.search_songs_by_title(list(self._songs.values()), title)
40    
41    def get_song_recommendations(self) -> List[Song]:
42        return self._recommendation_service.generate_recommendations(list(self._songs.values()))
43    
44    def get_player(self) -> Player:
45        return self._player

4.12 MusicStreamingDemo

The demo class validates the entire system by simulating various user interactions.

1class MusicStreamingDemo:
2    @staticmethod
3    def main():
4        system = MusicStreamingSystem.get_instance()
5        
6        # --- Setup Catalog ---
7        daft_punk = Artist("art1", "Daft Punk")
8        system.add_artist(daft_punk)
9        
10        discovery = Album("Discovery")
11        s1 = system.add_song("s1", "One More Time", daft_punk.id, 320)
12        s2 = system.add_song("s2", "Aerodynamic", daft_punk.id, 212)
13        s3 = system.add_song("s3", "Digital Love", daft_punk.id, 301)
14        s4 = system.add_song("s4", "Radioactive", daft_punk.id, 311)
15        discovery.add_track(s1)
16        discovery.add_track(s2)
17        discovery.add_track(s3)
18        discovery.add_track(s4)
19        
20        # --- Register Users (Builder Pattern) ---
21        free_user = User.Builder("Alice").with_subscription(SubscriptionTier.FREE, 0).build()
22        premium_user = User.Builder("Bob").with_subscription(SubscriptionTier.PREMIUM, 0).build()
23        system.register_user(free_user)
24        system.register_user(premium_user)
25        
26        # --- Observer Pattern: User follows artist ---
27        print("--- Observer Pattern Demo ---")
28        premium_user.follow_artist(daft_punk)
29        daft_punk.release_album(discovery)  # This will notify Bob
30        print()
31        
32        # --- Strategy Pattern: Playback behavior ---
33        print("--- Strategy Pattern (Free vs Premium) & State Pattern (Player) Demo ---")
34        player = system.get_player()
35        player.load(discovery, free_user)
36        
37        # --- Command Pattern: Controlling the player ---
38        play = PlayCommand(player)
39        pause = PauseCommand(player)
40        next_track = NextTrackCommand(player)
41        
42        play.execute()  # Plays song 1
43        next_track.execute()  # Plays song 2
44        pause.execute()  # Pauses song 2
45        play.execute()  # Resumes song 2
46        next_track.execute()  # Plays song 3
47        next_track.execute()  # Plays song 4 (ad for free user)
48        print()
49        
50        # --- Premium user experience (no ads) ---
51        print("--- Premium User Experience ---")
52        player.load(discovery, premium_user)
53        play.execute()
54        next_track.execute()
55        print()
56        
57        # --- Composite Pattern: Play a playlist ---
58        print("--- Composite Pattern Demo ---")
59        my_playlist = Playlist("My Awesome Mix")
60        my_playlist.add_track(s3)  # Digital Love
61        my_playlist.add_track(s1)  # One More Time
62        
63        player.load(my_playlist, premium_user)
64        play.execute()
65        next_track.execute()
66        print()
67        
68        # --- Search and Recommendation ---
69        print("--- Search and Recommendation Service Demo ---")
70        search_results = system.search_songs_by_title("love")
71        print(f"Search results for 'love': {search_results}")
72        
73        recommendations = system.get_song_recommendations()
74        print(f"Your daily recommendations: {recommendations}")
75
76if __name__ == "__main__":
77    MusicStreamingDemo.main()

5. Run and Test

Files28
commands
entities
enums
observers
services
states
strategies
music_streaming_demo.py
main
music_streaming_system.py
music_streaming_demo.pymain
Output

6. Quiz

Design Spotify - Quiz

1 / 20
Multiple Choice

Which entity allows users to manage and organize collections of songs in a music streaming service design?

How helpful was this article?

Comments


0/2000

No comments yet. Be the first to comment!

Copilot extension content script